package org.roussev.hiena.sound;
/*
* Copyright 2009 Hiena Mp3 Player http://code.google.com/p/hiena-mp3-player/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import java.io.IOException;
import java.io.File;
import java.io.InputStream;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.util.Map;
import java.util.StringTokenizer;
import java.net.URL;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioFileFormat;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.SourceDataLine;
import javax.sound.sampled.FloatControl;
import javax.sound.sampled.UnsupportedAudioFileException;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javazoom.spi.mpeg.sampled.file.MpegAudioFileFormat;
import javazoom.spi.mpeg.sampled.file.MpegAudioFormat;
public class ABasicPlayer implements Runnable {
private static final int EXTERNAL_BUFFER_SIZE = 4096 * 4;
private Thread m_thread = null;
private Object m_dataSource;
private AudioInputStream m_audioInputStream;
private AudioFileFormat m_audioFileFormat;
private SourceDataLine m_line;
private FloatControl m_gainControl;
private FloatControl m_panControl;
//---------
public static final int PLAYING = 0;
public static final int PAUSED = 1;
public static final int STOPPED = 2;
public static final int READY = 3;
private int m_status = READY;
private ABasicPlayerListener m_bpl = null;
private boolean isApplet;
public int debugLevel = 0;
//---------------------------------------------
public ABasicPlayer() {
m_dataSource = null;
m_audioInputStream = null;
m_audioFileFormat = null;
m_line = null;
m_gainControl = null;
m_panControl = null;
}
//---------------------------------------------
public ABasicPlayer(int debugLevel) {
this();
this.debugLevel = debugLevel;
}
//---------------------------------------------
public ABasicPlayer(ABasicPlayerListener bpl) {
this();
m_bpl = bpl;
}
//---------------------------------------------
public ABasicPlayer(ABasicPlayerListener bpl, int debugLevel) {
this(debugLevel);
m_bpl = bpl;
}
//---------------------------------------------
public final int getStatus() {
return m_status;
}
//---------------------------------------------
public final void setApplet(boolean isApplet) {
this.isApplet = isApplet;
}
//---------------------------------------------
public final String getStatusInfo() {
if(m_status == PAUSED) {
return "PAUSED";
} else if(m_status == PLAYING) {
return "PLAYING";
} else if(m_status == STOPPED) {
return "STOPPED";
}
return "NONE";
}
//---------------------------------------------
public final void setDataSource(Object source) throws UnsupportedAudioFileException, LineUnavailableException, IOException {
if (source != null) {
m_dataSource = source;
initAudioInputStream();
}
}
//---------------------------------------------
private final void initAudioInputStream() throws UnsupportedAudioFileException, LineUnavailableException, IOException {
if (m_dataSource instanceof URL) {
initAudioInputStream((URL) m_dataSource);
}
else if (m_dataSource instanceof File) {
initAudioInputStream((File) m_dataSource);
}
}
//---------------------------------------------
/**
* Inits Audio ressources from file.
*/
private final void initAudioInputStream(File file) throws UnsupportedAudioFileException, IOException {
if(isApplet) {
// Applet UGLY workaround.
m_audioInputStream = AppletMpegSPIWorkaround.getAudioInputStream(file);
m_audioFileFormat = AppletMpegSPIWorkaround.getAudioFileFormat(file);
} else {
m_audioInputStream = AudioSystem.getAudioInputStream(file);
m_audioFileFormat = AudioSystem.getAudioFileFormat(file);
}
}
//---------------------------------------------
/**
* Inits Audio ressources from URL.
*/
private final void initAudioInputStream(URL url) throws UnsupportedAudioFileException, IOException {
if(isApplet) {
// Applet UGLY workaround.
m_audioInputStream = AppletMpegSPIWorkaround.getAudioInputStream(url);
m_audioFileFormat = AppletMpegSPIWorkaround.getAudioFileFormat(url);
} else {
m_audioInputStream = AudioSystem.getAudioInputStream(url);
m_audioFileFormat = AudioSystem.getAudioFileFormat(url);
}
}
//---------------------------------------------
/**
* Inits Audio ressources from AudioSystem.<br>
* DateSource must be present.
*/
protected final void initLine() throws LineUnavailableException {
if (m_line == null) {
createLine();
trace(1,"---", "Create Line OK ");
openLine();
}
else {
final AudioFormat lineAudioFormat = m_line.getFormat();
final AudioFormat audioInputStreamFormat = (m_audioInputStream == null)? null : m_audioInputStream.getFormat();
if (!lineAudioFormat.equals(audioInputStreamFormat)) {
m_line.close();
openLine();
}
}
}
//---------------------------------------------
/**
* Inits a DateLine.<br>
*
* We check if the line supports Volume and Pan controls.
*
* From the AudioInputStream, i.e. from the sound file, we
* fetch information about the format of the audio data. These
* information include the sampling frequency, the number of
* channels and the size of the samples. There information
* are needed to ask JavaSound for a suitable output line
* for this audio file.
* Furthermore, we have to give JavaSound a hint about how
* big the internal buffer for the line should be. Here,
* we say AudioSystem.NOT_SPECIFIED, signaling that we don't
* care about the exact size. JavaSound will use some default
* value for the buffer size.
*/
private final void createLine() throws LineUnavailableException {
if (m_line == null) {
final AudioFormat sourceFormat = m_audioInputStream.getFormat();
//trace(1,"---", "Source format : ", sourceFormat.toString());
final AudioFormat targetFormat = new AudioFormat(
AudioFormat.Encoding.PCM_SIGNED,
sourceFormat.getSampleRate(),
16,
sourceFormat.getChannels(),
sourceFormat.getChannels() * 2,
sourceFormat.getSampleRate(),
false
);
//trace(1,"---", "Target format: " + targetFormat);
if(isApplet) {
// Applet UGLY workaround.
m_audioInputStream = AppletMpegSPIWorkaround.getAudioInputStream(targetFormat,m_audioInputStream);
} else {
m_audioInputStream = AudioSystem.getAudioInputStream(targetFormat, m_audioInputStream);
}
final AudioFormat audioFormat = m_audioInputStream.getFormat();
trace(1,"---", "Create Line : ", audioFormat.toString());
DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat, AudioSystem.NOT_SPECIFIED);
m_line = (SourceDataLine) AudioSystem.getLine(info);
/*-- Display supported controls jdk_1.5. dont supports them ?! --* /
Outil.p(" :::::::::::: Controls : " );
Control[] c = m_line.getControls();
for (int p=0; p < c.length; p++) {
Outil.p( "Controls : " + c[p].toString() );
trace(2,"---", "Controls : " + c[p].toString() );
}*/
/*-- Is Gain Control supported ? --*/
if (m_line.isControlSupported(FloatControl.Type.MASTER_GAIN)) {
m_gainControl = (FloatControl) m_line.getControl(FloatControl.Type.MASTER_GAIN);
trace(1,"---", "Master Gain Control : ["+m_gainControl.getMinimum()+","+m_gainControl.getMaximum()+"]",""+m_gainControl.getPrecision());
}
/*-- Is Pan control supported ? --*/
if (m_line.isControlSupported(FloatControl.Type.PAN)) {
m_panControl = (FloatControl) m_line.getControl(FloatControl.Type.PAN);
trace(1,"---", "Pan Control : ["+ m_panControl.getMinimum()+","+m_panControl.getMaximum()+"]",""+m_panControl.getPrecision());
}
}
}
//---------------------------------------------
/**
* Opens the line.
*/
private final void openLine() throws LineUnavailableException {
if (m_line != null) {
final AudioFormat audioFormat = m_audioInputStream.getFormat();
trace(1,"---", "AudioFormat : "+audioFormat);
m_line.open(audioFormat, m_line.getBufferSize());
}
}
//---------------------------------------------
/**
* Stops the playback.<br>
*
* Player Status = STOPPED.<br>
* Thread should free Audio ressources.
*/
public final void stopPlayback() {
if ( (m_status == PLAYING) || (m_status == PAUSED) ) {
if (m_line != null) {
//doFade_DOWN(STOPPED);
m_line.flush();
m_line.stop();
}
m_status = STOPPED;
trace(1,"---", "Stop called");
}
}
//---------------------------------------------
/**
* Pauses the playback.<br>
*
* Player Status = PAUSED.
*/
public final void pausePlayback() {
if (m_line != null) {
if (m_status == PLAYING) {
doStop(PAUSED);
trace(1,"===================", "Pause called");
}
}
}
//---------------------------------------------
private final void doStop(int status) {
m_line.flush();
m_line.stop();
m_status = status;
}
//---------------------------------------------
/**
* Resumes the playback.<br>
*
* Player Status = PLAYING.
*/
public final void resumePlayback() {
if (m_line != null) {
if (m_status == PAUSED) {
m_line.start();
m_status = PLAYING;
//trace(1,"=====================", "Resume called");
}
}
}
//---------------------------------------------
/**
* Starts playback.
*/
public final String startPlayback() throws javax.sound.sampled.LineUnavailableException {
if ((m_status == STOPPED) || (m_status == READY)) {
trace(1,"---", "Start called");
if (!(m_thread == null || !m_thread.isAlive())) {
trace(1,"---", "WARNING: old thread still running!!");
int cnt = 0;
while (m_status != READY) {
try {
if (m_thread != null) {
cnt++;
Thread.sleep(1000);
if (cnt > 2) m_thread.interrupt();
}
} catch (InterruptedException e) {
e.printStackTrace();
trace(1,"---", "Waiting Error : "+e.getMessage());
}
trace(1,"---", "Waiting ... "+cnt);
}
}
initLine();
trace(1,"---", "Creating new thread");
m_thread = new Thread(this, "BasicPlayer_Thread" );
m_thread.start();
if (m_line != null) {
m_line.start();
}
}
return null;
}
//---------------------------------------------
/**
* Main loop.
*
* Player Status == STOPPED => End of Thread + Freeing Audio Ressources.<br>
* Player Status == PLAYING => Audio stream data sent to Audio line.<br>
* Player Status == PAUSED => Waiting for another status.
*/
public final void run() {
trace(1,"---", "Thread Running");
//if (m_audioInputStream.markSupported()) m_audioInputStream.mark(m_audioFileFormat.getByteLength());
//else trace(1,"---", "Mark not supported");
int nBytesRead = 1;
m_status = PLAYING;
int nBytesCursor = 0;
final byte[] abData = new byte[EXTERNAL_BUFFER_SIZE];
final float nFrameSize = (float) m_line.getFormat().getFrameSize();
final float nFrameRate = m_line.getFormat().getFrameRate();
final float bytesPerSecond = nFrameSize*nFrameRate;
//final int secondsTotal = (int) Math.round(getTotalLengthInSeconds());
//secondsTotal = Math.round((float)m_audioFileFormat.getByteLength()/bytesPerSecond);
//------
while ( (nBytesRead != -1) && (m_status != STOPPED) ) {
if (m_status == PLAYING) {
try {
nBytesRead = m_audioInputStream.read(abData, 0, abData.length);
}
catch (IOException e) {
trace(1,"---", "InputStream error : ("+nBytesRead+")",e.getMessage());
e.printStackTrace();
m_status = STOPPED;
} catch (ArrayIndexOutOfBoundsException e) {
trace(1,"---", "Internal error : ",e.getMessage());
e.printStackTrace();
m_status = STOPPED;
// -- ?! --
/*java.lang.ArrayIndexOutOfBoundsException: 580
at javazoom.jl.decoder.LayerIIIDecoder.huffman_decode(Unknown Source)
at javazoom.jl.decoder.LayerIIIDecoder.decode(Unknown Source)
at javazoom.jl.decoder.LayerIIIDecoder.decodeFrame(Unknown Source)
at javazoom.jl.decoder.Decoder.decodeFrame(Unknown Source)
at javazoom.spi.mpeg.sampled.convert.DecodedMpegAudioInputStream.execute(Unknown Source)
at org.tritonus.share.TCircularBuffer.read(TCircularBuffer.java:134)
at org.tritonus.share.sampled.convert.TAsynchronousFilteredAudioInputStream.read(TAsynchronousFilteredAudioInputStream.java:189)
*/
}
if (nBytesRead >= 0) {
final int nBytesWritten = m_line.write(abData, 0, nBytesRead);
nBytesCursor = nBytesCursor + nBytesWritten;
if (m_bpl != null) {
final int secondsAmount = (int) Math.round((float)nBytesCursor/bytesPerSecond);
m_bpl.updateMediaCursor( secondsAmount );
}
}
}
else {
try {
Thread.sleep(100);
} catch (java.lang.InterruptedException e) {
e.printStackTrace();
trace(1,"---", "Thread cannot sleep : ",e.getMessage());
}
}
} // end of while
//------
if (m_line != null) {
try {
m_line.drain();
m_line.stop();
m_line.close();
} catch (Exception e) {
e.printStackTrace();
trace(1,"---", "Cannot Free Audio ressources",e.getMessage());
}
finally {
m_line = null;
}
}
trace(1,"---", "Thread Stopped");
m_status = READY;
// End Of Media
if (m_bpl != null) m_bpl.updateMediaState("EOM");
}
/*----------------------------------------------*/
/*-- Gain Control --*/
/*----------------------------------------------*/
/**
* Returns true if Gain control is supported.
*/
public final boolean hasGainControl() {
return m_gainControl != null;
}
/**
* Sets Gain value.
* Linear scale 0.0 <--> 1.0
* Threshold Coef. : 1/2 to avoid saturation.
*/
public final void setGain(double param) {
if (hasGainControl()) {
final double minGainDB = getMinimum();
final double ampGainDB = ((10.0f/20.0f)*getMaximum()) - getMinimum();
final double cste = Math.log(10.0)/20;
final double valueDB = minGainDB + (1/cste)*Math.log(1+(Math.exp(cste*ampGainDB)-1)*param);
m_gainControl.setValue((float)valueDB);
//trace(1,"---", "Gain : "+valueDB);
}
}
/**
* Returns Gain value.
*/
public final float getGain() {
if (hasGainControl()) {
return m_gainControl.getValue();
}
else {
return 0.0F;
}
}
/**
* Gets max Gain value.
*/
public final float getMaximum() {
if (hasGainControl()) {
return m_gainControl.getMaximum();
}
else {
return 0.0F;
}
}
/**
* Gets min Gain value.
*/
public final float getMinimum() {
if (hasGainControl()) {
return m_gainControl.getMinimum();
}
else {
return 0.0F;
}
}
/*----------------------------------------------*/
/*-- Pan Control --*/
/*----------------------------------------------*/
/**
* Returns true if Pan control is supported.
*/
public final boolean hasPanControl() {
return m_panControl != null;
}
//-------------------------
/**
* Returns Pan precision.
*/
public final float getPrecision() {
if (hasPanControl()) {
return m_panControl.getPrecision();
}
else {
return 0.0F;
}
}
//--------------------------
/**
* Returns Pan value.
*/
public final float getPan() {
if (hasPanControl()) {
return m_panControl.getValue();
}
else {
return 0.0F;
}
}
//------------------------------
/**
* Sets Pan value.
* Linear scale : -1.0 <--> +1.0
*/
public final void setPan(float fPan) {
if (hasPanControl()) {
m_panControl.setValue(fPan);
//trace(1,"---", "Pan : "+fPan);
}
}
/*----------------------------------------------*/
/*-- Audio Format --*/
/*----------------------------------------------*/
//-------------------------------------------------------
public final AudioFormat getAudioFormat() {
if (m_audioFileFormat != null) {
return m_audioFileFormat.getFormat();
}
else {
return null;
}
}
//-------------------------------------------------------
public final AudioFileFormat getAudioFileFormat() {
if (m_audioFileFormat != null) {
return m_audioFileFormat;
}
else {
return null;
}
}
//---------------------------------------------
public final double getTotalLengthInSeconds() {
double lengthInSecond = 0.0;
if ( getAudioFileFormat() != null) {
final int FL = (getAudioFileFormat()).getFrameLength();
final int FS = (getAudioFormat()).getFrameSize();
final float SR = (getAudioFormat()).getSampleRate();
//final float FR = (getAudioFormat()).getFrameRate();
final int TL = (getAudioFileFormat()).getByteLength();
final String type = (getAudioFileFormat()).getType().toString();
//final String encoding = (getAudioFormat()).getEncoding().toString();
//-----
if ( FL != -1 && type.startsWith("MP3") ) {
final MpegAudioFileFormat fileFormat = ((MpegAudioFileFormat)getAudioFileFormat());
final Map map = fileFormat.properties();
final long l = ((Long)map.get("duration")).longValue();
lengthInSecond = l/1000000;
}
//-----
else if ( FL != -1 && type.startsWith("VORBIS") ) {
// No accurate formula :-(
// Vorbis SPi does not have yet VorbisAudioFileFormat.
// Assumes that type includes xBitRate string.
// Old dirty solution with SPI.
try {
final StringTokenizer st = new StringTokenizer(type,"x");
st.nextToken();
st.nextToken();
final String totalMSStr = st.nextToken();
lengthInSecond = Math.round((Integer.parseInt(totalMSStr))/1000);
}
catch(java.util.NoSuchElementException e) { e.printStackTrace(); }
}
//-----
else {
final int br = getBitRate();
if (br > 0) lengthInSecond = TL*8/br;
else lengthInSecond = TL/(FS*SR);
}
//trace(2,"---","Type="+type+" Encoding="+encoding+" FL="+FL+" FS="+FS+" SR="+SR+" FR="+FR+" TL="+TL," lenghtInSecond="+lenghtInSecond);
}
if (lengthInSecond < 0.0) lengthInSecond = 0.0;
return lengthInSecond;
}
//---------------------------------------------
public final Map getMpegFormatProperties() {
if ( getAudioFileFormat() != null && getAudioFormat() != null ) {
final String type = (getAudioFileFormat()).getType().toString();
if ( (type != null) && ((type.startsWith("MP3")) ) ) {
final MpegAudioFormat audioFormat = ((MpegAudioFormat)getAudioFormat());
return audioFormat.properties();
}
}
return null;
}
//---------------------------------------------
public final Map getMpegFileFormatProperties() {
if ( getAudioFileFormat() != null) {
final String type = (getAudioFileFormat()).getType().toString();
if ( (type != null) && ((type.startsWith("MP3")) ) ) {
final MpegAudioFileFormat audiofileFormat = ((MpegAudioFileFormat)getAudioFileFormat());
return audiofileFormat.properties();
}
}
return null;
}
//---------------------------------------------
public final int getBitRate() {
int bitRate = 0;
if ( getAudioFileFormat() != null) {
//final int FL = (getAudioFileFormat()).getFrameLength();
final int FS = (getAudioFormat()).getFrameSize();
//final float SR = (getAudioFormat()).getSampleRate();
final float FR = (getAudioFormat()).getFrameRate();
//final int TL = (getAudioFileFormat()).getByteLength();
final String type = (getAudioFileFormat()).getType().toString();
//final String encoding = (getAudioFormat()).getEncoding().toString();
if ( (type != null) && ((type.startsWith("MP3")) ) ) {
final MpegAudioFormat audioFormat = ((MpegAudioFormat)getAudioFormat());
final Map map = audioFormat.properties();
bitRate = ((Integer)map.get("bitrate")).intValue();
}
else if ( type != null && type.startsWith("VORBIS") ) {
// Dirty solution :-(.
try {
StringTokenizer st = new StringTokenizer(type,"x");
if (st.hasMoreTokens()) {
st.nextToken();
String bitRateStr = st.nextToken();
bitRate = Math.round((Integer.parseInt(bitRateStr)));
}
} catch(Exception ignore) { System.out.println(ignore); }
}
else {
bitRate = Math.round(FS*FR*8);
}
//trace(2,"---","Type="+type+" Encoding="+encoding+" FL="+FL+" FS="+FS+" SR="+SR+" FR="+FR+" TL="+TL," bitRate="+bitRate);
}
// N/A so computes bitRate for output.
if ((bitRate <= 0) && (m_line != null)) {
bitRate = Math.round(((m_line.getFormat()).getFrameSize())*((m_line.getFormat()).getFrameRate())*8);
}
return bitRate;
}
//---------------------------------------------
protected final InputStream openInput(File file) throws IOException {
return new BufferedInputStream( new FileInputStream(file));
}
/*----------------------------------------------*/
/*-- Misc --*/
/*----------------------------------------------*/
/**
* Sends traces to Debug.
*/
private final void trace(int level, String msg1, String msg2) {
if( debugLevel >= level )
System.out.println( msg1 + " : " + msg2 );
}
private final void trace(int level, String msg1, String msg2, String msg3) {
if( debugLevel >= level )
System.out.println( msg1 + " : " + msg2 + " : " + msg3);
}
//---------------------------------------------
public final void dbg() {
System.out.println( "[ AudioFileFormat = " + getAudioFileFormat() );
System.out.println( "[ AudioFormat = " + getAudioFormat() );
System.out.println( "[ BitRate = " + getBitRate() );
System.out.println( "[ Gain = " + getGain() );
System.out.println( "[ Maximum = " + getMaximum() );
System.out.println( "[ Minimum = " + getMinimum() );
System.out.println( "[ Pan = " + getPan() );
System.out.println( "[ Precision = " + getPrecision() );
System.out.println( "[ Status = " + getStatus() );
}
//---------------------------------------------
}